﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;


namespace Xtensible.Extensions.Mapping
{
    public static class DataTableExtensions
    {
        /// <summary>
        ///   Sets the target property value.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "targetProperty">The target property.</param>
        /// <param name = "targetValue">The target value.</param>
        /// <param name = "destination">The destination.</param>
        private static void SetValue<T>(PropertyInfo targetProperty, object targetValue, T destination)
        {
            if (targetValue != DBNull.Value)
                targetProperty.SetValue(destination, targetValue, null);
        }

        /// <summary>
        ///   Gets the name of the column.
        /// </summary>
        /// <param name = "targetPropertyName">Name of the target property.</param>
        /// <param name = "aliases">The aliases.</param>
        /// <returns></returns>
        private static string GetColumnName(string targetPropertyName, Aliases aliases)
        {
            if (aliases != null && aliases.Keys.Any(k => k == targetPropertyName))
            {
                return aliases[targetPropertyName];
            }

            return targetPropertyName;
        }

        /// <summary>
        ///   Gets the source property
        /// </summary>
        private static object GetColumnValue(Aliases aliases, PropertyInfo targetProperty,
                                             Exclusions exclusions, DataRow sourceRow)
        {
            object value = new NoValue();

            if (aliases != null && aliases.Keys.Contains(targetProperty.Name))
            {
                //Excluded properties should not be mapped... even if aliased
                if (!(exclusions != null && Helper.ContainsExclusion(aliases[targetProperty.Name], exclusions)))
                    value = sourceRow[aliases[targetProperty.Name]];
            }
            else
            {
                // If target property is already aliased then do not match on source property of the same name... this will cause a double mapping
                if (aliases == null || (!aliases.Values.Contains(targetProperty.Name)))
                {
                    //do not map excluded properties
                    if (!(exclusions != null && Helper.ContainsExclusion(targetProperty.Name, exclusions)))
                    {
                        if (sourceRow.Table.Columns.Contains(targetProperty.Name))
                            value = sourceRow[targetProperty.Name];
                    }
                }
            }
            return value;
        }


        /// <summary>
        ///   Maps data table to object list.
        /// </summary>
        /// <typeparam name = "T">The destination object type.  Must have a default constructor.</typeparam>
        /// <param name = "source">The source data table.</param>
        /// <param name = "dataTypeTriggers">The generic data triggers (null for no trigger).</param>
        /// <param name = "aliases">Property name aliases ([destination name, source name])  Can be null.</param>
        /// <param name = "exclusions">Source object properties that should not be mapped.  Can be null.</param>
        /// <returns></returns>
        public static IEnumerable<T> MapToObjectList<T>(this DataTable source,
                                                        DataTypeTriggers dataTypeTriggers, Aliases aliases,
                                                        Exclusions exclusions) where T : new()
        {
            var list = new List<T>();

            var properties = Helper.GetProperties<T>();

            foreach (DataRow row in source.Rows)
            {
                var destination = new T();

                foreach (var targetProperty in properties)
                {
                    var targetValue = GetColumnValue(aliases, targetProperty, exclusions, row);

                    if (!(targetValue is NoValue))
                    {
                        var type = source.Columns[GetColumnName(targetProperty.Name, aliases)].DataType;

                        if (dataTypeTriggers != null && dataTypeTriggers.Keys.Contains(type))
                        {
                            if (targetValue != DBNull.Value)
                                targetValue = dataTypeTriggers[type](targetValue);
                        }

                        SetValue(targetProperty, targetValue, destination);
                    }
                }
                list.Add(destination);
            }
            return list;
        }


        /// <summary>
        ///   Maps data table to object list.
        /// </summary>
        /// <typeparam name = "T">The destination object type.  Must have a default constructor.</typeparam>
        /// <param name = "source">The source data table.</param>
        /// <param name = "propertyNameTriggers">The column name triggers.  Key is based on the column name.</param>
        /// <param name = "aliases">Property name aliases ([destination name, source name])  Can be null.</param>
        /// <param name = "exclusions">Source object properties that should not be mapped.  Can be null.</param>
        /// <returns></returns>
        public static IEnumerable<T> MapToObjectList<T>(this DataTable source,
                                                        PropertyNameTriggers propertyNameTriggers, Aliases aliases,
                                                        Exclusions exclusions) where T : new()
        {
            var list = new List<T>();

            var properties = Helper.GetProperties<T>();

            foreach (DataRow row in source.Rows)
            {
                var destination = new T();

                foreach (var targetProperty in properties)
                {
                    var targetValue = GetColumnValue(aliases, targetProperty, exclusions, row);

                    if (!(targetValue is NoValue))
                    {
                        var columnName = GetColumnName(targetProperty.Name, aliases);
                        if (propertyNameTriggers != null && propertyNameTriggers.Keys.Contains(columnName))
                        {
                            if (targetValue != DBNull.Value)
                                targetValue = propertyNameTriggers[columnName](targetValue);
                        }

                        SetValue(targetProperty, targetValue, destination);
                    }
                }
                list.Add(destination);
            }
            return list;
        }

        /// <summary>
        ///   Maps data table to object list.
        /// </summary>
        /// <typeparam name = "T">The destination object type.  Must have a default constructor.</typeparam>
        /// <param name = "source">The source data table.</param>
        /// <param name = "aliases">Property name aliases ([destination name, source name])  Can be null.</param>
        /// <param name = "exclusions">Source object properties that should not be mapped.  Can be null.</param>
        /// <returns></returns>
        public static IEnumerable<T> MapToObjectList<T>(this DataTable source,
                                                        Aliases aliases,
                                                        Exclusions exclusions) where T : new()
        {
            return MapToObjectList<T>(source, new PropertyNameTriggers(), aliases, exclusions);
        }

        #region Nested type: NoValue

        private class NoValue
        {
        }

        #endregion
    }
}